Description
Pasting text into glint's editor via terminal-native paste (bracketed paste, e.g. Cmd+V) is broken: only the first character appears, then the UI freezes; sometimes the rest of the paste eventually lands, sometimes only part of it does. Distinct from the explicit Ctrl+V clipboard.ReadAll() path (app.go paste()/pasteText()), which is unaffected. Root cause not yet found โ candidates being investigated: whether bracketed paste is actually landing as a single tea.KeyMsg (Paste:true) vs falling back to per-character delivery, and whether some per-keystroke path (looksLikeMouseLeak guard, spellcheck/buildVisual rebuild, auto-close handling) stalls mid-stream.
Acceptance Criteria
- #1 Root cause of the freeze/partial-paste is identified with evidence (not guessed)
- #2 Pasting a multi-line block of text via terminal-native paste inserts the full text without freezing or dropping characters
- #3 Regression test added that reproduces the failure mode and passes after the fix
Implementation Notes
Root cause: buildVisual()'s memoized e.visual cache is only invalidated once per HandleKey/InsertText call (before dispatch), but multi-rune inserts (paste) looped InsertRune()/InsertNewline() per character, each calling followCursor()->buildVisual(). The FIRST such call mid-loop rebuilds+caches the visual model against a half-inserted buffer; every later call in the same loop sees a non-nil cache and reuses that stale snapshot, and nothing invalidates it again afterward. e.Lines ends up with the full pasted text, but the screen (and any render until the next real keystroke) shows only the state as of the first inserted rune -- reproduces exactly as reported: first char shows, then freezes, until a later keypress forces a fresh rebuild ('adds the rest in'). Second, related bug: the dispatch() KeyRunes/KeySpace loops called InsertRune() for every rune including literal '\n' bytes from a multi-line paste, embedding raw newlines inside one Lines[] entry instead of splitting lines (InsertText already special-cased '\n' -> InsertNewline(), the raw paste dispatch path did not). Confirmed both with throwaway repro tests before fixing (buildVisual returned only 'h' of 'hello world'; a 'line1\nline2' paste stayed a single Lines[] entry). Fix: added insertRuneRaw/insertNewlineRaw (mutation only, no followCursor) and a bulk InsertRunes([]rune) that splices all runes/newlines first and calls followCursor()/buildVisual() exactly once at the end. Repointed dispatch()'s KeyRunes/KeySpace paste branches and InsertText onto InsertRunes. Added regression tests in cache_test.go: TestPasteKeyMsgReflectsFullTextImmediately, TestPasteKeyMsgSplitsEmbeddedNewlines, TestInsertTextReflectsFullTextImmediately. Full suite (go build, go vet, go test ./...) passes.
Final Summary
Fixed the paste-freezes-on-first-char bug. Root cause: the memoized buildVisual() cache got rebuilt against a half-inserted buffer mid-paste (each InsertRune in the multi-rune loop called followCursor->buildVisual, caching after the first char with nothing invalidating it again), so the screen stayed stuck on the first pasted character even though the buffer had the full text. Also fixed a related bug where multi-line pastes embedded raw newlines in one Lines[] entry instead of splitting lines. Added InsertRunes() bulk-insert path (single followCursor call, correct newline splitting) used by both the terminal bracketed-paste dispatch path and InsertText (Ctrl+V). Verified with new regression tests in cache_test.go and full go build/vet/test.